/*
 * 代号：凤凰
 * http://www.jphenix.org
 * 2022年9月3日
 * V4.0
 */
package com.jphenix.webserver.instancea;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import com.jphenix.share.tools.MD5;
import com.jphenix.standard.docs.ClassInfo;
import com.jphenix.standard.servlet.api.IResponse;
import com.jphenix.webserver.api.RequestImpl;
import com.jphenix.webserver.api.ResponseImpl;

/**
 * 输出静态资源
 * 
 * 2022-09-04 重构了内置Servlet容器，不依赖外部ServletApi包
 * 
 * @author MBG
 * 2022年9月3日
 */
@ClassInfo({"2022-09-04 21:58","输出静态资源"})
public class FileServe {
	
	private final String ARG_LIST_FILE = "listings"; // 在没选中文件时，是否显示文件列表


	private Serve    kernel            = null; // 核心类实例
	private int      noListFile        = 0;    // 是否不显示文件列表  1显示 2不显示 0未初始化
	private String[] DEFAULTINDEXPAGES = {"index.html","index.htm", "default.htm", "default.html" }; //默认静态资源文件名
	private String   charSet           = "ISO-8859-1"; // 默认编码格式
	
	private final SimpleDateFormat shortfmt          = new SimpleDateFormat("MMM dd HH:mm");
	private final SimpleDateFormat longfmt           = new SimpleDateFormat("MMM dd yyyy");
	private final String           BGCOLOR           = "BGCOLOR=\"#D1E9FE\""; // 默认页面背景颜色
	private final DecimalFormat    lengthftm         = new DecimalFormat("#");
	private final int              COPY_BUF_SIZE     = 4096 * 2; //处理流缓存大小
	
	/**
	 * 构造函数
	 * @author MBG
	 */
	public FileServe(Serve serve) {
		super();
		this.kernel = serve;
	}

	/**
	 * 输出静态资源
	 * @param req         请求对象
	 * @param res         反馈对象
	 * @throws Exception  异常
	 * 2022年9月3日
	 * @author MBG
	 */
    public void fileServe(RequestImpl req, ResponseImpl res) throws Exception {
		boolean headOnly;
		headOnly = "head".equalsIgnoreCase(req.getMethod());
		String path = canonicalizePath(req.getPathInfo());
		dispatchPathName(req, res, headOnly, path);
	}
	
	/**
	 * 输出指定文件内容到页面中
	 * 刘虻
	 * 2012-7-27 上午9:09:36
	 * @param req						页面请求
	 * @param res						页面反馈
	 * @param headOnly			是否只有报文头
	 * @param path					文件路径
	 * @param file						文件对象
	 * @throws IOException		异常
	 */
	private void serveFile(RequestImpl req, ResponseImpl res,
			boolean headOnly, String path, File file) throws IOException {
		kernel.log("getting " + file);
		if (!file.canRead()) {
			res.sendError(IResponse.SC_FORBIDDEN);
			return;
		} else {
            try {
                file.getCanonicalPath();
            } catch (Exception e) {
                res.sendError(IResponse.SC_FORBIDDEN,
                        "Forbidden, exception:" + e);
                return;
            }
        }
		res.setStatus(IResponse.SC_OK);
		long lastMod = file.lastModified();
		String ifModSinceStr = req.getHeader("If-Modified-Since");
		long ifModSince = -1;
		if (ifModSinceStr != null) {
			int semi = ifModSinceStr.indexOf(';');
			if (semi != -1) {
                ifModSinceStr = ifModSinceStr.substring(0, semi);
            }
			try {
				ifModSince = DateFormat.getDateInstance().parse(ifModSinceStr)
						.getTime();
			} catch (Exception ignore) {
				kernel.log("Can't parse " + ifModSinceStr + " " + ignore);
			}
		}
		if (ifModSince != -1 && ifModSince >= lastMod) {
			res.setStatus(IResponse.SC_NOT_MODIFIED);
			headOnly = true;
		}

		res.setContentType(kernel.getServletContext().getMimeType(file.getName()));
		if (file.length() < Integer.MAX_VALUE) {
			res.setContentLength((int) file.length());
		}else {
			res.setHeader("Content-Length", Long.toString(file.length()));
		}
		
		res.setDateHeader("Last-modified", lastMod);
		res.setHeader("ETag",MD5.md5(file.getPath()));

		OutputStream out = null;
		InputStream in = null;
		try {
			out = res.iGetOutputStream();
			if (!headOnly) {
				in = new FileInputStream(file);
				copyStream(in, out);
			}
		} finally {
			if (in != null) {
                try {
                    in.close();
                } catch (IOException ioe) {
                }
            }
			if (out != null) {
				out.flush();
				out.close();
			}
		}
	}

	/**
	 * 处理流
	 * 刘虻
	 * 2012-7-27 上午9:14:27
	 * @param in           读入流
	 * @param out          输出流
	 * @throws IOException 异常
	 */
	private void copyStream(InputStream in, OutputStream out) throws IOException {
		byte[] buf = new byte[COPY_BUF_SIZE];
		int len;
		while ((len = in.read(buf)) != -1) {
			out.write(buf, 0, len);
		}
	}

	/**
	 * 处理目标文件相对路径
	 * 刘虻
	 * 2012-7-27 上午9:01:40
	 * @param path 页面请求中的文件路径信息
	 * @return 处理后的文件相对路径
	 */
	private String canonicalizePath(String path) {
		if (path == null || path.length() == 0) {
            return path;
        }
		List<String> pathElems = new ArrayList<String>(6);
		char[] pa = path.toCharArray();
		int n = pa.length;
		int s = -1;
		int lev = 0;
		for (int i = 0; i < n; i++) {
			if (s < 0) {
				if (pa[i] != '/' && pa[i] != '\\') {
                    s = i;
                }
			} else {
				boolean f = false;
				if (pa[i] == '?') {
                    f = true;
                }
				if (pa[i] == '/' || pa[i] == '\\' || f) {
					String el = new String(pa, s, i - s);
					if ("..".equals(el)) {
						if (pathElems.size() > 0) {
                            pathElems.remove(pathElems.size() - 1);
                        } else {
                            lev--;
                        }
					} else if (".".equals(el) == false) {
                        if (lev >= 0) {
                            pathElems.add(el);
                        } else {
                            lev++;
                        }
                    }
					if (f) {
						s = i;
						break;
					}
					s = -1;
				}
			}
		}
		if (s > 0) {
			String el = new String(pa, s, n - s);
			if ("..".equals(el)) {
				if (pathElems.size() > 0) {
                    pathElems.remove(pathElems.size() - 1);
                }
			} else if (".".equals(el) == false) {
                if (lev >= 0) {
                    pathElems.add(el);
                }
            }
		} else {
            pathElems.add("");
        }
		if (pathElems.size() == 0) {
            return "";
        }
		StringBuffer result = new StringBuffer(n);
		result.append(pathElems.get(0));
		n = pathElems.size();
		for (int i = 1; i < n; i++) {
            result.append('/').append(pathElems.get(i));
        }
		return result.toString();
	}
	
	/**
	 * 解析目标文件
	 * 刘虻
	 * 2012-7-27 上午9:04:46
	 * @param req							页面请求
	 * @param res							页面反馈
	 * @param headOnly				只有报文头
	 * @param path						文件路径
	 * @throws IOException			异常
	 */
	private void dispatchPathName(RequestImpl req, ResponseImpl res, boolean headOnly, String path)
			throws IOException {
		String filename = req.getPathTranslated() != null ? req
				.getPathTranslated().replace('/', File.separatorChar) : "";
		File file = new File(filename);
		kernel.log("showing '" + filename + "' for path " + path);
		if (file.exists()) {
			if (!file.isDirectory()) {
				serveFile(req, res, headOnly, path, file);
			} else {
				if(isListFile()) {
					kernel.log("showing dir " + file);
					if (redirectDirectory(req, res, path, file) == false) {
						showIdexFile(req, res, headOnly, path, filename);
					}
				}else {
					res.sendError(IResponse.SC_NOT_FOUND);
				}
			}
		} else {
			System.out.println("The URL Not Found ["+req.getRequestURL()+"] referer:["+req.getHeader("Referer")+"] FilePath:["+filename+"]");
			res.sendError(IResponse.SC_NOT_FOUND,"The URL Not Found");
		}
	}
	
	/**
	 * 显示默认静态资源信息
	 */
	protected void showIdexFile(RequestImpl req, ResponseImpl res, boolean headOnly, String path,
			String parent) throws IOException {
		kernel.log("showing index in directory " + parent);
		for (int i = 0; i < DEFAULTINDEXPAGES.length; i++) {
			File indexFile = new File(parent, DEFAULTINDEXPAGES[i]);
			if (indexFile.exists()) {
				serveFile(req, res, headOnly, path, indexFile);
				return;
			}
		}
		serveDirectory(req, res, headOnly, path, new File(parent));
	}
	
	/**
	 * 母鸡
	 * 
	 * @author 刘虻 2008-7-9上午08:19:30
	 * @param req
	 * @param res
	 * @param headOnly
	 * @param path
	 * @param file
	 * @throws IOException
	 */
	protected void serveDirectory(RequestImpl req, ResponseImpl res, boolean headOnly, String path, File file)
			throws IOException {
		kernel.log("indexing " + file);
		if (!file.canRead()) {
			res.sendError(IResponse.SC_FORBIDDEN);
			return;
		}
		if(!isListFile()) {
			res.sendError(IResponse.SC_NOT_FOUND);
			return;
		}
		if(path.toLowerCase().startsWith("web-inf")) {
			res.sendError(IResponse.SC_NOT_FOUND);
			return;
		}
		res.setStatus(IResponse.SC_OK);
		res.setContentType("text/html;charset=" + charSet);
		OutputStream out = res.iGetOutputStream();
		if (!headOnly) {
			PrintStream p = new PrintStream(new BufferedOutputStream(out),false, charSet);
			p.println("<HTML><HEAD>");
			p
					.println("<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset="
							+ charSet + "\">");
			p.println("<TITLE>Index of " + path + "</TITLE>");
			p.println("</HEAD><BODY " + BGCOLOR);
			p.println("><H2>Index of " + path + "</H2>");
			p.println("<PRE>");
			p.println("mode         bytes  last-changed    name");
			p.println("<HR>");
			String[] names = file.list();
			Arrays.sort(names);
			long total = 0;
			for (int i = 0; i < names.length; ++i) {
				if("web-inf".equals(names[i].toLowerCase())) {
					continue;
				}
				File aFile = new File(file, names[i]);
				String aFileType;
				long aFileLen;
				if (aFile.isDirectory()) {
                    aFileType = "d";
                } else if (aFile.isFile()) {
                    aFileType = "-";
                } else {
                    aFileType = "?";
                }
				String aFileRead = (aFile.canRead() ? "r" : "-");
				String aFileWrite = (aFile.canWrite() ? "w" : "-");
				String aFileExe = "-";
				String aFileSize = lengthftm.format(aFileLen = aFile.length());
				total += (aFileLen + 1023) / 1024;
				while (aFileSize.length() < 12) {
                    aFileSize = " " + aFileSize;
                }
				String aFileDate = lsDateStr(new Date(aFile.lastModified()));
				while (aFileDate.length() < 14) {
                    aFileDate += " ";
                }
				String aFileDirsuf = (aFile.isDirectory() ? "/" : "");
				String aFileSuf = (aFile.isDirectory() ? "/" : "");
				p.println(aFileType + aFileRead + aFileWrite + aFileExe + "  "
						+ aFileSize + "  " + aFileDate + "  " + "<A HREF=\""
						+ URLEncoder.encode(names[i], charSet) /* 1.4 */
						+ aFileDirsuf + "\">" + names[i] + aFileSuf + "</A>");
			}
			if (total != 0) {
                total += 3;
            }
			p.println("total " + total);
			p.println("</PRE>");
			p.println("<HR>");
			Identification.writeAddress(p);
			p.println("</BODY></HTML>");
			p.flush();
		}
		out.close();
	}
	
	/**
	 * 重定向
	 */
	private boolean redirectDirectory(RequestImpl req, ResponseImpl res, String path, File file) throws IOException {
		int pl = path.length();
		if (pl > 0 && path.charAt(pl - 1) != '/') {
			int sp = path.lastIndexOf('/');
			if (sp < 0) {
                path += '/';
            } else {
                path = path.substring(sp + 1) + '/';
            }
			kernel.log("redirecting dir " + path);
			res.sendRedirect(path);
			return true;
		}
		return false;
	}
	
	/**
	 * 整理输出时间
	 */
	private String lsDateStr(Date date) {
		if (Math.abs(System.currentTimeMillis() - date.getTime()) < 183L * 24L
				* 60L * 60L * 1000L) {
			return shortfmt.format(date);
		} else {
			return longfmt.format(date);
		}
	}

	/**
	 * 是否不显示文件列表
	 * 默认不显示列表
	 * @author 刘虻
	 * 2009-9-22下午02:17:54
	 * @return 是否不显示文件列表
	 */
	private boolean isListFile() {
		//noListFile: 是否不显示文件列表  1显示 2不显示 0未初始化
		if(noListFile==0) {
			if(kernel.getServeParameter().getArgumentBoolean(ARG_LIST_FILE)) {
				noListFile = 1;
			}else {
				noListFile = 2;
			}
		}
		return noListFile == 1;
	}

}
